{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "lined-delhi",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 495 ms (started: 2021-08-18 01:07:00 +08:00)\n"
     ]
    }
   ],
   "source": [
    "# 自动计算cell的计算时间\n",
    "%load_ext autotime\n",
    "\n",
    "%matplotlib inline\n",
    "%config InlineBackend.figure_format='svg' #矢量图设置，让绘图更清晰"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "id": "congressional-kernel",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "origin\tgit@github.com:ustchope/handson-ml2.git (fetch)\n",
      "origin\tgit@github.com:ustchope/handson-ml2.git (push)\n",
      "[master 5763c46] 更新 ch13 #2  Aug 18, 2021\n",
      " 1 file changed, 2218 insertions(+), 1 deletion(-)\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "To git@github.com:ustchope/handson-ml2.git\n",
      "   44828bc..5763c46  master -> master\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.61 s (started: 2021-08-18 03:20:11 +08:00)\n"
     ]
    }
   ],
   "source": [
    "%%bash\n",
    "\n",
    "# 增加更新\n",
    "git add *.ipynb *.md\n",
    "\n",
    "git remote -v\n",
    "\n",
    "git commit -m '更新 ch13 #2  Aug 18, 2021'\n",
    "\n",
    "git push origin master"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "copyrighted-istanbul",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.78 s (started: 2021-08-18 01:07:29 +08:00)\n"
     ]
    }
   ],
   "source": [
    "#设置使用的gpu\n",
    "import tensorflow as tf\n",
    "\n",
    "gpus = tf.config.list_physical_devices(\"GPU\")\n",
    "\n",
    "if gpus:\n",
    "   \n",
    "    gpu0 = gpus[0] #如果有多个GPU，仅使用第0个GPU\n",
    "    tf.config.experimental.set_memory_growth(gpu0, True) #设置GPU显存用量按需使用\n",
    "    # 或者也可以设置GPU显存为固定使用量(例如：4G)\n",
    "    #tf.config.experimental.set_virtual_device_configuration(gpu0,\n",
    "    #    [tf.config.experimental.VirtualDeviceConfiguration(memory_limit=4096)]) \n",
    "    tf.config.set_visible_devices([gpu0],\"GPU\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "nasty-rouge",
   "metadata": {},
   "source": [
    "# 使用 TensorFlow 加载和预处理数据"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "christian-uzbekistan",
   "metadata": {},
   "source": [
    "到目前为止，我们只使用了适合内存的数据集，但深度学习系统通常在不适合 RAM 的非常大的数据集上进行训练。使用其他深度学习库来实现获取大型数据集并对其进行有效预处理可能会很棘手，但由于数据 API，TensorFlow 使这件事变得容易：您只需创建一个数据集对象，并告诉它从哪里获取数据以及如何转换它. TensorFlow 负责所有实现细节，例如多线程、排队、批处理和预取。此外，数据 API 与 tf.keras 无缝协作！\n",
    "\n",
    "现成的 Data API 可以读取文本文件（例如 CSV 文件）、具有固定大小记录的二进制文件以及使用 TensorFlow 的 TFRecord 格式的二进制文件，该格式支持不同大小的记录。 TFRecord 是一种灵活高效的二进制格式，通常包含协议缓冲区（一种开源二进制格式）。数据 API 还支持从 SQL 数据库读取数据。此外，许多开源扩展可用于从各种数据源中读取数据，例如 Google 的 BigQuery 服务。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "growing-structure",
   "metadata": {},
   "source": [
    "高效读取庞大的数据集并不是唯一的困难：数据还需要进行预处理，通常是归一化。 此外，它并不总是严格由方便的数字字段组成：可能有文本特征、分类特征等。 这些需要进行编码，例如使用单热编码、词袋编码或嵌入（正如我们将看到的，嵌入是表示类别或标记的可训练密集向量）。 处理所有这些预处理的一种选择是编写您自己的自定义预处理层。 另一种是使用 Keras 提供的标准预处理层。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "robust-noise",
   "metadata": {},
   "source": [
    "在本章中，我们将介绍数据 API、TFRecord 格式，以及如何创建自定义预处理层和使用标准 Keras 层。 我们还将快速浏览一下 TensorFlow 生态系统中的一些相关项目：\n",
    "* TF 变换 (tf.Transform)  \n",
    "使编写一个预处理函数成为可能，该函数可以在您的完整训练集上以批处理模式运行，在训练之前（以加快速度），然后导出到 TF 函数并合并到您的训练模型中，以便一旦部署 在生产中，它可以即时处理新实例。\n",
    "* TF 数据集 (TFDS)  \n",
    "提供了一个方便的功能来下载各种常见的数据集，包括像 ImageNet 这样的大型数据集，以及使用 Data API 操作它们的方便的数据集对象。\n",
    "\n",
    "让我们开始吧！"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fallen-share",
   "metadata": {},
   "source": [
    "## Data API"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "automotive-landscape",
   "metadata": {},
   "source": [
    "整个数据 API 围绕数据集的概念展开：正如您可能怀疑的那样，这表示数据项的序列。 通常你会使用从磁盘逐渐读取数据的数据集，但为了简单起见，让我们使用 tf.data.Dataset.from_tensor_slices() 创建一个完全在 RAM 中的数据集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "adequate-blast",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 1.47 ms (started: 2021-08-18 01:34:25 +08:00)\n"
     ]
    }
   ],
   "source": [
    "X = tf.range(10) # any data tensor"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "decreased-romantic",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<TensorSliceDataset shapes: (), types: tf.int32>"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.91 ms (started: 2021-08-18 01:34:26 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dataset = tf.data.Dataset.from_tensor_slices(X)\n",
    "dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "gothic-metro",
   "metadata": {},
   "source": [
    "from_tensor_slices() 函数接受一个张量并创建一个 tf.data.Dataset，其元素是 X 的所有切片（沿第一维），因此该数据集包含 10 个项目：张量 0、1、2、...、9。在 在这种情况下，如果我们使用 tf.data.Dataset.range(10)，我们将获得相同的数据集。\n",
    "\n",
    "您可以简单地迭代数据集的项目，如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "wrapped-jersey",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tf.Tensor(0, shape=(), dtype=int32)\n",
      "tf.Tensor(1, shape=(), dtype=int32)\n",
      "tf.Tensor(2, shape=(), dtype=int32)\n",
      "tf.Tensor(3, shape=(), dtype=int32)\n",
      "tf.Tensor(4, shape=(), dtype=int32)\n",
      "tf.Tensor(5, shape=(), dtype=int32)\n",
      "tf.Tensor(6, shape=(), dtype=int32)\n",
      "tf.Tensor(7, shape=(), dtype=int32)\n",
      "tf.Tensor(8, shape=(), dtype=int32)\n",
      "tf.Tensor(9, shape=(), dtype=int32)\n",
      "time: 11.1 ms (started: 2021-08-18 01:34:29 +08:00)\n"
     ]
    }
   ],
   "source": [
    "for item in dataset:\n",
    "    print(item)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "flexible-pharmacology",
   "metadata": {},
   "source": [
    "### 链式转换"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "hearing-western",
   "metadata": {},
   "source": [
    "拥有数据集后，您可以通过调用其转换方法对其应用各种转换。 每个方法都返回一个新数据集，因此您可以像这样链接转换（此链如图 13-1 所示）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "collectible-premiere",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tf.Tensor([0 1 2 3 4 5 6], shape=(7,), dtype=int32)\n",
      "tf.Tensor([7 8 9 0 1 2 3], shape=(7,), dtype=int32)\n",
      "tf.Tensor([4 5 6 7 8 9 0], shape=(7,), dtype=int32)\n",
      "tf.Tensor([1 2 3 4 5 6 7], shape=(7,), dtype=int32)\n",
      "tf.Tensor([8 9], shape=(2,), dtype=int32)\n",
      "time: 12.9 ms (started: 2021-08-18 01:34:35 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dataset = dataset.repeat(3).batch(7)\n",
    "for item in dataset:\n",
    "    print(item)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "precise-absence",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtkasai5eij61dw0lstbi02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "composed-humor",
   "metadata": {},
   "source": [
    "在这个例子中，我们首先对原始数据集调用repeat()方法，它返回一个新的数据集，该数据集将原始数据集的项目重复3次。 当然，这不会将内存中的所有数据复制三遍！ （如果不带参数调用此方法，新数据集将永远重复源数据集，因此迭代数据集的代码必须决定何时停止。）然后我们在这个新数据集上调用 batch() 方法， 这再次创建了一个新的数据集。 这将把前一个数据集的项目分成七个项目的批次。 最后，我们迭代这个最终数据集的项目。 如您所见，batch() 方法必须输出大小为 2 而不是 7 的最后一批，但是如果您希望它删除最后一批以便所有批次具有完全相同的大小，则可以使用 drop_remainder=True 调用它 ."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adopted-ivory",
   "metadata": {},
   "source": [
    "> 数据集方法不会修改数据集，而是创建新数据集，因此请确保保留对这些新数据集的引用（例如，使用 dataset = ...），否则什么都不会发生。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "married-importance",
   "metadata": {},
   "source": [
    "您还可以通过调用 map() 方法来转换项目。 例如，这将创建一个所有项目都翻倍的新数据集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "powerful-moore",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<MapDataset shapes: (), types: tf.int64>"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 11.4 ms (started: 2021-08-18 01:36:02 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dataset = tf.data.Dataset.range(10)\n",
    "dataset = dataset.map(lambda x: x * 2) # Items: [0,2,4,6,8,10,12]\n",
    "dataset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "comparable-collar",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<FilterDataset shapes: (), types: tf.int64>\n",
      "<FilterDataset shapes: (), types: tf.int64>\n",
      "<FilterDataset shapes: (), types: tf.int64>\n",
      "time: 26.5 ms (started: 2021-08-18 01:37:44 +08:00)\n"
     ]
    }
   ],
   "source": [
    "for item in dataset.take(3):\n",
    "    print(dataset)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "overhead-miracle",
   "metadata": {},
   "source": [
    "您将调用此函数将您想要的任何预处理应用于您的数据。 有时这将包括可能非常密集的计算，例如重塑或旋转图像，因此您通常希望生成多个线程以加快速度：就像设置 num_parallel_calls 参数一样简单。 请注意，您传递给 map() 方法的函数必须可转换为 TF 函数（请参阅第 12 章）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "boolean-short",
   "metadata": {},
   "source": [
    "map() 方法将转换应用于每个项目，而 apply() 方法将转换应用于整个数据集。 例如，以下代码将 unbatch() 函数应用于数据集（该函数目前处于实验阶段，但很可能会在未来版本中移至核心 API）。 新数据集中的每个项目将是一个单整数张量，而不是一批七个整数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "given-notion",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<_UnbatchDataset shapes: (), types: tf.int32>"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 11.6 ms (started: 2021-08-18 01:35:39 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dataset = dataset.unbatch() # Items: 0,2,4,...\n",
    "dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "musical-credits",
   "metadata": {},
   "source": [
    "也可以使用 filter() 方法简单地过滤数据集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "photographic-newport",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<FilterDataset shapes: (), types: tf.int64>"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 28.6 ms (started: 2021-08-18 01:36:34 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dataset = dataset.filter(lambda x: x < 10) # Items: 0 2 4 6 8 0 2 4 6...\n",
    "dataset"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "naked-production",
   "metadata": {},
   "source": [
    "您通常只想查看数据集中的几个项目。 您可以使用 take() 方法："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "skilled-tonight",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tf.Tensor(0, shape=(), dtype=int64)\n",
      "tf.Tensor(2, shape=(), dtype=int64)\n",
      "tf.Tensor(4, shape=(), dtype=int64)\n",
      "time: 25.4 ms (started: 2021-08-18 01:37:55 +08:00)\n"
     ]
    }
   ],
   "source": [
    "for item in dataset.take(3):\n",
    "    print(item)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "palestinian-reflection",
   "metadata": {},
   "source": [
    "### 打乱数据"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "japanese-lighting",
   "metadata": {},
   "source": [
    "如您所知，当训练集中的实例独立且分布相同时，梯度下降效果最好（参见第 4 章）。确保这一点的一种简单方法是使用 shuffle() 方法对实例进行混洗。它将创建一个新数据集，该数据集首先用源数据集的第一项填充缓冲区。然后，每当它被要求提供一个项目时，它会从缓冲区中随机取出一个项目，并用源数据集中的新项目替换它，直到它完全遍历源数据集。此时它会继续从缓冲区中随机取出项目，直到它为空。您必须指定缓冲区大小，并且使其足够大很重要，否则改组将不会很有效。只是不要超过您拥有的 RAM 量，即使您有足够的 RAM，也没有必要超出数据集的大小。如果您每次运行程序时都想要相同的随机顺序，您可以提供一个随机种子。例如，以下代码创建并显示一个包含 0 到 9 整数的数据集，重复 3 次，使用大小为 5 的缓冲区和 42 的随机种子进行混洗，并以批量大小为 7 进行批处理："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "meaning-occasions",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.39 ms (started: 2021-08-18 01:40:09 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dataset = tf.data.Dataset.range(10).repeat(3) # 0 to 9, three times"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "portable-microphone",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 21.3 ms (started: 2021-08-18 01:40:17 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dataset = dataset.shuffle(buffer_size=5, seed=42).batch(7)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "hybrid-stocks",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "tf.Tensor([0 2 3 6 7 9 4], shape=(7,), dtype=int64)\n",
      "tf.Tensor([5 0 1 1 8 6 5], shape=(7,), dtype=int64)\n",
      "tf.Tensor([4 8 7 1 2 3 0], shape=(7,), dtype=int64)\n",
      "tf.Tensor([5 4 2 7 8 9 9], shape=(7,), dtype=int64)\n",
      "tf.Tensor([3 6], shape=(2,), dtype=int64)\n",
      "time: 12.1 ms (started: 2021-08-18 01:40:30 +08:00)\n"
     ]
    }
   ],
   "source": [
    "for item in dataset:\n",
    "    print(item)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "three-muscle",
   "metadata": {},
   "source": [
    "如果您在混洗数据集上调用 repeat() ，默认情况下它会在每次迭代时生成一个新订单。 这通常是一个好主意，但如果您更喜欢在每次迭代中重用相同的顺序（例如，用于测试或调试），您可以设置 reshuffle_each_iteration=False。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "final-persian",
   "metadata": {},
   "source": [
    "对于不适合内存的大型数据集，这种简单的混洗缓冲区方法可能不够用，因为缓冲区与数据集相比较小。一种解决方案是混洗源数据本身（例如，在 Linux 上，您可以使用 shuf 命令混洗文本文件）。这肯定会大大改善洗牌！即使源数据被打乱了，您通常也希望将其打乱一些，否则每个时期都会重复相同的顺序，并且模型最终可能会出现偏差（例如，由于一些偶然出现的虚假模式）源数据的顺序）。为了更多地打乱实例，一种常见的方法是将源数据拆分为多个文件，然后在训练期间以随机顺序读取它们。但是，位于同一文件中的实例最终仍会彼此靠近。为避免这种情况，您可以随机选择多个文件并同时读取它们，交错它们的记录。然后最重要的是您可以使用 shuffle() 方法添加一个混洗缓冲区。如果所有这些听起来工作量很大，请不要担心：Data API 只需几行代码就可以让这一切成为可能。让我们看看如何做到这一点。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "decimal-nigeria",
   "metadata": {},
   "source": [
    "### 来自多个文件的交错行"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "disciplinary-invitation",
   "metadata": {},
   "source": [
    "首先，假设您已加载加利福尼亚住房数据集，对其进行打乱（除非它已经打乱），并将其拆分为训练集、验证集和测试集。 然后将每个集合拆分为多个 CSV 文件，每个文件如下所示（每行包含八个输入特征加上目标房屋价值中值）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "instructional-newark",
   "metadata": {},
   "outputs": [],
   "source": [
    "MedInc,HouseAge,AveRooms,AveBedrms,Popul,AveOccup,Lat,Long,MedianHouseValue\n",
    "3.5214,15.0,3.0499,1.1065,1447.0,1.6059,37.63,-122.43,1.442\n",
    "5.3275,5.0,6.4900,0.9910,3464.0,3.4433,33.69,-117.39,1.687\n",
    "3.1,29.0,7.5423,1.5915,1328.0,2.2508,38.44,-122.98,1.621\n",
    "[...]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "functioning-tonight",
   "metadata": {},
   "source": [
    "我们还假设 train_filepaths 包含训练文件路径列表（并且您还有 valid_filepaths 和 test_filepaths）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "blessed-nurse",
   "metadata": {},
   "outputs": [],
   "source": [
    "train_filepaths\n",
    "['datasets/housing/my_train_00.csv', 'datasets/housing/my_train_01.csv',...]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "frequent-license",
   "metadata": {},
   "source": [
    "或者，您可以使用文件模式； 例如，train_filepaths = \"datasets/housing/my_train_*.csv\"。 现在让我们创建一个仅包含这些文件路径的数据集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "hawaiian-aggregate",
   "metadata": {},
   "outputs": [],
   "source": [
    "filepath_dataset = tf.data.Dataset.list_files(train_filepaths, seed=42)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "smoking-madagascar",
   "metadata": {},
   "source": [
    "默认情况下，list_files() 函数返回一个对文件路径进行混洗的数据集。 一般来说，这是一件好事，但如果您出于某种原因不想这样做，您可以设置 shuffle=False。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "prescription-guinea",
   "metadata": {},
   "source": [
    "接下来，您可以调用 interleave() 方法一次从五个文件中读取并交错它们的行（使用 skip() 方法跳过每个文件的第一行，即标题行）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "thrown-mistress",
   "metadata": {},
   "outputs": [],
   "source": [
    "n_readers = 5\n",
    "dataset = filepath_dataset.interleave(\n",
    "    lambda filepath: tf.data.TextLineDataset(filepath).skip(1),\n",
    "    cycle_length=n_readers)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "acting-telephone",
   "metadata": {},
   "source": [
    "interleave() 方法将创建一个数据集，该数据集将从 filepath_dataset 中提取五个文件路径，并且对于每个文件路径，它将调用您提供的函数（在本例中为 lambda）以创建一个新数据集（在本例中为 TextLineDataset） . 需要明确的是，在这个阶段总共会有七个数据集：文件路径数据集、交错数据集以及交错数据集内部创建的五个 TextLineDataset。 当我们迭代交错数据集时，它将循环遍历这五个 TextLineDatasets，一次读取一行，直到所有数据集都没有项目。 然后它将从 filepath_dataset 中获取接下来的五个文件路径并以相同的方式交错它们，依此类推，直到用完文件路径。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "capital-satisfaction",
   "metadata": {},
   "source": [
    "> 为了使交错效果最佳，最好使用相同长度的文件； 否则最长文件的结尾将不会被交错。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "civilian-norman",
   "metadata": {},
   "source": [
    "默认情况下， interleave() 不使用并行性； 它一次只从每个文件中读取一行，顺序。 如果您希望它实际并行读取文件，您可以将 num_parallel_calls 参数设置为您想要的线程数（注意 map() 方法也有这个参数）。 您甚至可以将其设置为 tf.data.experimental.AUTOTUNE 以使 TensorFlow 根据可用 CPU 动态选择正确数量的线程（但是，目前这是一个实验性功能）。 现在让我们看看数据集包含的内容："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "sharing-crossing",
   "metadata": {},
   "outputs": [],
   "source": [
    "for line in dataset.take(5):\n",
    "    print(line.numpy())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "continuous-parameter",
   "metadata": {},
   "source": [
    "这些是随机选择的五个 CSV 文件的第一行（忽略标题行）。 看起来挺好的！ 但正如您所见，这些只是字节字符串； 我们需要解析它们并缩放数据。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "continuing-baptist",
   "metadata": {},
   "source": [
    "### 预处理数据"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "competitive-secondary",
   "metadata": {},
   "source": [
    "让我们实现一个小函数来执行此预处理："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "id": "welcome-microwave",
   "metadata": {},
   "outputs": [],
   "source": [
    "X_mean, X_std = [...] # 训练集中每个特征的均值和尺度\n",
    "n_inputs = 8"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "id": "spiritual-republican",
   "metadata": {},
   "outputs": [
    {
     "ename": "SyntaxError",
     "evalue": "invalid syntax (<ipython-input-45-d08e14272cd4>, line 3)",
     "output_type": "error",
     "traceback": [
      "\u001b[0;36m  File \u001b[0;32m\"<ipython-input-45-d08e14272cd4>\"\u001b[0;36m, line \u001b[0;32m3\u001b[0m\n\u001b[0;31m    fields = tf.io.decode_csv(line, record_defaults=defs) x = tf.stack(fields[:-1])\u001b[0m\n\u001b[0m                                                          ^\u001b[0m\n\u001b[0;31mSyntaxError\u001b[0m\u001b[0;31m:\u001b[0m invalid syntax\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 2.17 ms (started: 2021-08-18 02:17:14 +08:00)\n"
     ]
    }
   ],
   "source": [
    "def preprocess(line):\n",
    "    defs = [0.] * n_inputs + [tf.constant([], dtype=tf.float32)]\n",
    "    fields = tf.io.decode_csv(line, record_defaults=defs) x = tf.stack(fields[:-1])\n",
    "    y = tf.stack(fields[-1:])\n",
    "    return (x - X_mean) / X_std, y"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "liquid-training",
   "metadata": {},
   "source": [
    "让我们来看看这段代码：\n",
    "* 首先，代码假设我们已经预先计算了训练集中每个特征的均值和标准差。 X_mean 和 X_std 只是包含八个浮点数的一维张量（或 NumPy 数组），每个输入特征一个。\n",
    "* preprocess() 函数采用一个CSV 行并从解析它开始。为此，它使用 tf.io.decode_csv() 函数，该函数接受两个参数：第一个是要解析的行，第二个是包含 CSV 文件中每一列的默认值的数组。这个数组不仅告诉 TensorFlow 每一列的默认值，还告诉 TensorFlow 的数量和它们的类型。在这个例子中，我们告诉它所有特征列都是浮点数并且缺失值应该默认为 0，但是我们提供了一个类型为 tf.float32 的空数组作为最后一列（目标）的默认值：该数组告诉 TensorFlow该列包含浮点数，但没有默认值，因此如果遇到缺失值，它将引发异常。\n",
    "* decode_csv() 函数返回标量张量列表（每列一个），但我们需要返回一维张量数组。所以我们在除最后一个（目标）之外的所有张量上调用 tf.stack() ：这会将这些张量堆叠成一维数组。然后我们对目标值执行相同的操作（这使其成为具有单个值的一维张量数组，而不是标量张量）。\n",
    "* decode_csv() 函数返回标量张量列表（每列一个），但我们需要返回一维张量数组。 所以我们在除最后一个（目标）之外的所有张量上调用 tf.stack() ：这会将这些张量堆叠成一维数组。 然后我们对目标值执行相同的操作（这使其成为具有单个值的一维张量数组，而不是标量张量）。\n",
    "* 最后，我们通过减去特征均值然后除以特征标准差来缩放输入特征，我们返回一个包含缩放特征和目标的元组。\n",
    "\n",
    "让我们测试一下这个预处理函数："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "id": "tender-storm",
   "metadata": {},
   "outputs": [
    {
     "ename": "NameError",
     "evalue": "name 'preprocess' is not defined",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mNameError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-44-7023f80a213d>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mpreprocess\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mb'4.2083,44.0,5.3232,0.9171,846.0,2.3370,37.47,-122.2,2.782'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;31mNameError\u001b[0m: name 'preprocess' is not defined"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 44.9 ms (started: 2021-08-18 02:17:07 +08:00)\n"
     ]
    }
   ],
   "source": [
    "preprocess(b'4.2083,44.0,5.3232,0.9171,846.0,2.3370,37.47,-122.2,2.782')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "worst-strategy",
   "metadata": {},
   "source": [
    "看起来挺好的！ 我们现在可以将该函数应用于数据集。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "taken-gentleman",
   "metadata": {},
   "source": [
    "### 把所有东西放在一起"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "lightweight-calcium",
   "metadata": {},
   "source": [
    "为了使代码可重用，让我们将迄今为止讨论的所有内容放在一个小的辅助函数中：它将创建并返回一个数据集，该数据集将有效地从多个 CSV 文件中加载加州住房数据，对其进行预处理、洗牌、可选地重复它， 并批处理（见图 13-2）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ranging-niagara",
   "metadata": {},
   "outputs": [],
   "source": [
    "def csv_reader_dataset(filepaths, \n",
    "                       repeat=1, \n",
    "                       n_readers=5,\n",
    "                       n_read_threads=None, \n",
    "                       shuffle_buffer_size=10000,\n",
    "                       n_parse_threads=5, batch_size=32):\n",
    "    dataset = tf.data.Dataset.list_files(filepaths)\n",
    "    dataset = dataset.interleave(\n",
    "        lambda filepath: tf.data.TextLineDataset(filepath).skip(1),\n",
    "        cycle_length=n_readers, num_parallel_calls=n_read_threads)\n",
    "    dataset = dataset.map(preprocess, num_parallel_calls=n_parse_threads)\n",
    "    dataset = dataset.shuffle(shuffle_buffer_size).repeat(repeat)\n",
    "    return dataset.batch(batch_size).prefetch(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "realistic-tuning",
   "metadata": {},
   "source": [
    "除了对性能很重要的最后一行 (prefetch(1)) 外，这段代码中的所有内容都应该有意义。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "deadly-humidity",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtkcc4m2ecj61080ncgp102.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "noticed-plastic",
   "metadata": {},
   "source": [
    "### Prefetching"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "attached-supplement",
   "metadata": {},
   "source": [
    "通过在最后调用 prefetch(1)，我们正在创建一个数据集，该数据集将尽最大努力始终领先一批。 换句话说，当我们的训练算法正在处理一个批次时，数据集已经在并行工作以准备好下一个批次（例如，从磁盘读取数据并对其进行预处理）。 这可以显着提高性能，如图 13-3 所示。 如果我们还确保加载和预处理是多线程的（通过在调用 interleave() 和 map() 时设置 num_parallel_calls），我们可以利用 CPU 上的多个内核，并希望比在 GPU 上运行训练步骤更短地准备一批数据 ："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "derived-humanitarian",
   "metadata": {},
   "source": [
    "通过在最后调用 prefetch(1)，我们正在创建一个数据集，该数据集将尽最大努力始终领先一批。 换句话说，当我们的训练算法正在处理一个批次时，数据集已经在并行工作以准备好下一个批次（例如，从磁盘读取数据并对其进行预处理）。 这可以显着提高性能，如图 13-3 所示。 如果我们还确保加载和预处理是多线程的（通过在调用 interleave() 和 map() 时设置 num_parallel_calls），我们可以利用 CPU 上的多个内核，并希望比在 GPU 上运行训练步骤更短地准备一批数据 ：这样 GPU 将几乎 100% 使用（除了从 CPU 到 GPU3 的数据传输时间），并且训练运行速度会快得多。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "beneficial-huntington",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtkcfh7aroj61060ta79c02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "lined-cheat",
   "metadata": {},
   "source": [
    "如果你打算购买 GPU 卡，它的处理能力和显存大小当然是非常重要的（尤其是大量 RAM 对计算机视觉至关重要）。 获得良好性能同样重要的是它的内存带宽； 这是它每秒可以进出其 RAM 的千兆字节数。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "urban-trace",
   "metadata": {},
   "source": [
    "如果数据集小到可以放入内存，则可以通过使用数据集的 cache() 方法将其内容缓存到 RAM 来显着加快训练速度。 您通常应该在加载和预处理数据之后，但在混洗、重复、批处理和预取之前执行此操作。 这样，每个实例只会被读取和预处理一次（而不是每个 epoch 一次），但数据在每个 epoch 仍然会以不同的方式进行洗牌，并且仍然会提前准备下一批。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "behavioral-sentence",
   "metadata": {},
   "source": [
    "您现在知道如何构建高效的输入管道以从多个文本文件加载和预处理数据。 我们已经讨论了最常见的数据集方法，但您可能还想了解更多方法：concatenate()、zip()、window()、reduce()、shard()、flat_map() 和 padded_batch() . 还有几个类方法：from_generator() 和 from_tensors()，它们分别从 Python 生成器或张量列表创建新数据集。 请查看 API 文档以获取更多详细信息。 另请注意，在 tf.data.experimental 中有可用的实验性功能，其中许多功能可能会在未来版本中成为核心 API（例如，查看 CsvDataset 类，以及 make_csv_dataset() 方法，它负责 推断每列的类型）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "adequate-pantyhose",
   "metadata": {},
   "source": [
    "### 将数据集与 tf.keras 一起使用"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "pediatric-timer",
   "metadata": {},
   "source": [
    "现在我们可以使用 csv_reader_dataset() 函数为训练集创建数据集。 请注意，我们不需要重复它，因为这将由 tf.keras 处理。 我们还为验证集和测试集创建数据集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "legitimate-roads",
   "metadata": {},
   "outputs": [],
   "source": [
    "train_set = csv_reader_dataset(train_filepaths)\n",
    "valid_set = csv_reader_dataset(valid_filepaths)\n",
    "test_set = csv_reader_dataset(test_filepaths)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "covered-chemistry",
   "metadata": {},
   "source": [
    "现在我们可以使用这些数据集简单地构建和训练 Keras 模型。 我们需要做的就是将训练和验证数据集传递给 fit() 方法，而不是 X_train、y_train、X_valid 和 y_valid："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "latest-terry",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = keras.models.Sequential([...])\n",
    "model.compile([...])\n",
    "model.fit(train_set, epochs=10, validation_data=valid_set)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "animated-spread",
   "metadata": {},
   "source": [
    "同样，我们可以将数据集传递给evaluate() 和predict() 方法："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "indie-windsor",
   "metadata": {},
   "outputs": [],
   "source": [
    "model.evaluate(test_set)\n",
    "\n",
    "new_set = test_set.take(3).map(lambda X, y: X) # pretend we have 3 new instances\n",
    "model.predict(new_set) # a dataset containing new instances"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "seventh-seating",
   "metadata": {},
   "source": [
    "与其他集合不同，new_set 通常不包含标签（如果包含，Keras 将忽略它们）。 请注意，在所有这些情况下，如果需要，您仍然可以使用 NumPy 数组而不是数据集（当然，它们需要先加载和预处理）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "animated-popularity",
   "metadata": {},
   "source": [
    "如果您想构建自己的自定义训练循环（如第 12 章所述），您可以非常自然地遍历训练集："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "employed-classics",
   "metadata": {},
   "outputs": [],
   "source": [
    "for X_batch, y_batch in train_set: \n",
    "    [...] # perform one Gradient Descent step"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exempt-building",
   "metadata": {},
   "source": [
    "事实上，甚至可以创建一个执行整个训练循环的 TF 函数（参见第 12 章）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "synthetic-education",
   "metadata": {},
   "outputs": [],
   "source": [
    "@tf.function\n",
    "def train(model, optimizer, loss_fn, n_epochs, [...]):\n",
    "    train_set = csv_reader_dataset(train_filepaths, repeat=n_epochs, [...])\n",
    "    for X_batch, y_batch in train_set:\n",
    "        with tf.GradientTape() as tape:\n",
    "            y_pred = model(X_batch)\n",
    "            main_loss = tf.reduce_mean(loss_fn(y_batch, y_pred))\n",
    "            loss = tf.add_n([main_loss] + model.losses)\n",
    "        grads = tape.gradient(loss, model.trainable_variables)\n",
    "        optimizer.apply_gradients(zip(grads, model.trainable_variables))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "photographic-spyware",
   "metadata": {},
   "source": [
    "恭喜，您现在已经知道如何使用 Data API 构建强大的输入管道了！ 但是，到目前为止，我们使用的都是CSV文件，它很常见，简单方便，但效率不高，对大型或复杂的数据结构（如图像或音频）的支持不是很好。 那么让我们看看如何改用 TFRecords。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "concerned-class",
   "metadata": {},
   "source": [
    "如果您对 CSV 文件（或您使用的任何其他格式）感到满意，则不必使用 TFRecords。 俗话说，没坏就别修！ 当训练期间的瓶颈是加载和解析数据时，TFRecords 很有用。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "broadband-intro",
   "metadata": {},
   "source": [
    "## TFRecord 格式"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "surprising-velvet",
   "metadata": {},
   "source": [
    "TFRecord 格式是 TensorFlow 的首选格式，用于存储大量数据并高效读取。 它是一种非常简单的二进制格式，只包含一系列不同大小的二进制记录（每个记录由一个长度、一个 CRC 校验和来检查长度是否被破坏，然后是实际数据，最后是一个 CRC 校验和） 数据）。 您可以使用 tf.io.TFRecordWriter 类轻松创建 TFRecord 文件："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "immediate-xerox",
   "metadata": {},
   "outputs": [],
   "source": [
    "with tf.io.TFRecordWriter(\"my_data.tfrecord\") as f: \n",
    "    f.write(b\"This is the first record\") \n",
    "    f.write(b\"And this is the second record\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "realistic-bookmark",
   "metadata": {},
   "source": [
    "然后您可以使用 tf.data.TFRecordDataset 来读取一个或多个 TFRecord 文件："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "polyphonic-introduction",
   "metadata": {},
   "outputs": [],
   "source": [
    "filepaths = [\"my_data.tfrecord\"]\n",
    "dataset = tf.data.TFRecordDataset(filepaths)\n",
    "for item in dataset:\n",
    "    print(item)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "addressed-opinion",
   "metadata": {},
   "source": [
    "这将输出："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "clear-wellington",
   "metadata": {},
   "outputs": [],
   "source": [
    "tf.Tensor(b'This is the first record', shape=(), dtype=string)\n",
    "tf.Tensor(b'And this is the second record', shape=(), dtype=string)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "treated-brazilian",
   "metadata": {},
   "source": [
    "> 默认情况下，TFRecordDataset 会一个一个读取文件，但您可以通过设置 num_parallel_reads 使其并行读取多个文件并交错它们的记录。 或者，您可以通过使用 list_files() 和 interleave() 获得与我们之前读取多个 CSV 文件相同的结果。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "disciplinary-estimate",
   "metadata": {},
   "source": [
    "### 压缩的 TFRecord 文件"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "coordinated-password",
   "metadata": {},
   "source": [
    "有时压缩 TFRecord 文件会很有用，尤其是当它们需要通过网络连接加载时。 您可以通过设置 options 参数来创建压缩的 TFRecord 文件："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "excited-stopping",
   "metadata": {},
   "outputs": [],
   "source": [
    "options = tf.io.TFRecordOptions(compression_type=\"GZIP\")\n",
    "\n",
    "with tf.io.TFRecordWriter(\"my_compressed.tfrecord\", options) as f: \n",
    "    [...]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "flush-bolivia",
   "metadata": {},
   "source": [
    "读取压缩的TFRecord文件时，需要指定压缩类型："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "comfortable-intersection",
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset = tf.data.TFRecordDataset([\"my_compressed.tfrecord\"],\n",
    "                                  compression_type=\"GZIP\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "digital-baseball",
   "metadata": {},
   "source": [
    "### Protocol Buffers 简介"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "caring-madrid",
   "metadata": {},
   "source": [
    "尽管每个记录可以使用您想要的任何二进制格式，但 TFRecord 文件通常包含序列化的协议缓冲区（也称为 protobufs）。 这是 Google 于 2001 年开发的一种可移植、可扩展且高效的二进制格式，并于 2008 年开源； protobufs 现在被广泛使用，特别是在 gRPC，谷歌的远程过程调用系统中。 它们是使用一种简单的语言定义的，如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "lesser-treat",
   "metadata": {},
   "outputs": [],
   "source": [
    "syntax = \"proto3\";\n",
    "message Person {\n",
    "    string name = 1;\n",
    "    int32 id = 2;\n",
    "    repeated string email = 3; \n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cloudy-swift",
   "metadata": {},
   "source": [
    "这个定义表示我们使用的是 protobuf 格式的第 3 版，它指定每个 Person 对象可以（可选）具有类型为 string 的名称、类型为 int32 的 id 和零个或多个类型为 string 的电子邮件字段。 数字 1、2 和 3 是字段标识符：它们将用于每条记录的二进制表示。 一旦在 .proto 文件中有定义，就可以编译它。 这需要 protoc，即 protobuf 编译器，在 Python（或其他一些语言）中生成访问类。 请注意，我们将使用的 protobuf 定义已经为您编译好了，它们的 Python 类是 TensorFlow 的一部分，因此您不需要使用 protoc。 您只需要知道如何在 Python 中使用 protobuf 访问类。 为了说明基础，让我们看一个简单的例子，它使用为 Person protobuf 生成的访问类（代码在注释中解释）："
   ]
  },
  {
   "cell_type": "markdown",
   "id": "legendary-personality",
   "metadata": {},
   "source": [
    "## 预处理输入特征"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "reduced-strengthening",
   "metadata": {},
   "source": [
    "为神经网络准备数据需要将所有特征转换为数值特征，通常将它们归一化等等。 特别是，如果您的数据包含分类特征或文本特征，则需要将它们转换为数字。 这可以在准备数据文件时使用您喜欢的任何工具（例如 NumPy、pandas 或 Scikit-Learn）提前完成。 或者，您可以在使用数据 API 加载数据时即时预处理数据（例如，使用数据集的 map() 方法，正如我们之前看到的），或者您可以直接在模型中包含一个预处理层。 现在让我们看看最后一个选项。\n",
    "\n",
    "例如，以下是如何使用 Lambda 层实现标准化层。 对于每个特征，它减去均值并除以其标准差（加上一个微小的平滑项以避免被零除）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "powered-marketplace",
   "metadata": {},
   "outputs": [],
   "source": [
    "means = np.mean(X_train, axis=0, keepdims=True)\n",
    "stds = np.std(X_train, axis=0, keepdims=True)\n",
    "eps = keras.backend.epsilon()\n",
    "model = keras.models.Sequential([keras.layers.Lambda(lambda inputs: (inputs - means) / (stds + eps)),\n",
    "[...] # other layers\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "entire-grade",
   "metadata": {},
   "source": [
    "这不是太难！ 但是，您可能更喜欢使用一个很好的自包含自定义层（很像 Scikit-Learn 的 StandardScaler），而不是让诸如 mean 和 stds 之类的全局变量悬空："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "large-remove",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Standardization(keras.layers.Layer):\n",
    "    def adapt(self, data_sample):\n",
    "        self.means_ = np.mean(data_sample, axis=0, keepdims=True)\n",
    "        self.stds_ = np.std(data_sample, axis=0, keepdims=True)\n",
    "    def call(self, inputs):\n",
    "        return (inputs - self.means_) / (self.stds_ + keras.backend.epsilon())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "stretch-skirt",
   "metadata": {},
   "source": [
    "在您可以使用此标准化层之前，您需要通过调用 adapt() 方法并将数据样本传递给它来使其适应您的数据集。 这将允许它为每个特征使用适当的均值和标准差："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "tested-tragedy",
   "metadata": {},
   "outputs": [],
   "source": [
    "std_layer = Standardization()\n",
    "std_layer.adapt(data_sample)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "introductory-spokesman",
   "metadata": {},
   "source": [
    "该样本必须足够大才能代表您的数据集，但它不必是完整的训练集：通常，随机选择几百个实例就足够了（但是，这取决于您的任务）。 接下来，您可以像使用普通层一样使用此预处理层："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "analyzed-disability",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = keras.Sequential()\n",
    "model.add(std_layer) [...] # create the rest of the model\n",
    "model.compile([...])\n",
    "model.fit([...])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "approximate-reference",
   "metadata": {},
   "source": [
    "如果您认为 Keras 应该包含这样的标准化层，那么这里有一些好消息给您：当您阅读本文时，keras.layers.Normalization 层可能已经可用。 它的工作方式与我们自定义的标准化层非常相似：首先，创建层，然后通过将数据样本传递给 adapt() 方法使其适应您的数据集，最后正常使用该层。\n",
    "\n",
    "现在让我们看看分类特征。 我们将首先将它们编码为 one-hot 向量。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "measured-dependence",
   "metadata": {},
   "source": [
    "### 使用 One-Hot 向量编码分类特征"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "spread-table",
   "metadata": {},
   "source": [
    "考虑我们在第 2 章中探索的加利福尼亚住房数据集中的 Ocean_proximity 特征：它是一个具有五个可能值的分类特征：“<1H OCEAN”、“INLAND”、“NEAR OCEAN”、“NEAR BAY”和“ISLAND”。 在将其提供给神经网络之前，我们需要对该特征进行编码。 由于类别很少，我们可以使用one-hot编码。 为此，我们首先需要将每个类别映射到它的索引（0 到 4），这可以使用查找表来完成："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "id": "sufficient-paraguay",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 7.11 ms (started: 2021-08-18 02:46:47 +08:00)\n"
     ]
    }
   ],
   "source": [
    "vocab = [\"<1H OCEAN\", \"INLAND\", \"NEAR OCEAN\", \"NEAR BAY\", \"ISLAND\"]\n",
    "indices = tf.range(len(vocab), dtype=tf.int64)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "id": "worth-burton",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.32 ms (started: 2021-08-18 02:47:00 +08:00)\n"
     ]
    }
   ],
   "source": [
    "table_init = tf.lookup.KeyValueTensorInitializer(vocab, indices)\n",
    "num_oov_buckets = 2\n",
    "table = tf.lookup.StaticVocabularyTable(table_init, num_oov_buckets)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "crazy-central",
   "metadata": {},
   "source": [
    "让我们来看看这段代码：\n",
    "* 我们首先定义词汇表：这是所有可能类别的列表。\n",
    "* 然后我们创建一个具有相应索引（0 到 4）的张量。\n",
    "* 接下来，我们为查找表创建一个初始化程序，将类别列表及其相应的索引传递给它。 在这个例子中，我们已经有了这些数据，所以我们使用了一个 KeyValueTensorInitializer； 但是如果类别在文本文件中列出（每行一个类别），我们将使用 TextFileInitializer 代替。\n",
    "* 在最后两行中，我们创建了查找表，为其提供初始值设定项并指定词表外 (oov) 桶的数量。 如果我们查找词汇表中不存在的类别，查找表将计算该类别的散列并使用它来将未知类别分配给 oov 存储桶之一。 它们的索引在已知类别之后开始，因此在此示例中，两个 oov 存储桶的索引是 5 和 6。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "academic-roots",
   "metadata": {},
   "source": [
    "为什么要使用 oov 存储桶？ 那么，如果类别的数量很大（例如，邮政编码、城市、单词、产品或用户）并且数据集也很大，或者它不断变化，那么获取类别的完整列表可能并不方便。 一种解决方案是根据数据样本（而不是整个训练集）定义词汇表，并为不在数据样本中的其他类别添加一些 oov 桶。 您希望在训练期间找到的未知类别越多，您应该使用的 oov 存储桶就越多。 确实，如果没有足够的 oov 桶，就会发生冲突：不同的类别最终会出现在同一个桶中，因此神经网络将无法区分它们（至少不是基于这个特征）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "exempt-carbon",
   "metadata": {},
   "source": [
    "现在让我们使用查找表将一小批分类特征编码为一个热向量："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "id": "canadian-spider",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 893 µs (started: 2021-08-18 02:49:34 +08:00)\n"
     ]
    }
   ],
   "source": [
    "category = tf.constant([\"NEAR BAY\", \"DESERT\", \"内陆\", \"内陆\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "id": "occupied-poultry",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 61.6 ms (started: 2021-08-18 02:49:51 +08:00)\n"
     ]
    }
   ],
   "source": [
    "cat_indices = table.lookup(category)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "id": "stupid-northwest",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(4,), dtype=int64, numpy=array([3, 5, 5, 5])>"
      ]
     },
     "execution_count": 51,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.33 ms (started: 2021-08-18 02:49:57 +08:00)\n"
     ]
    }
   ],
   "source": [
    " cat_indices"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "id": "entire-sphere",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.27 ms (started: 2021-08-18 02:50:05 +08:00)\n"
     ]
    }
   ],
   "source": [
    " cat_one_hot = tf.one_hot(cat_indices, depth=len(vocab) + num_oov_buckets)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "id": "actual-classics",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<tf.Tensor: shape=(4, 7), dtype=float32, numpy=\n",
       "array([[0., 0., 0., 1., 0., 0., 0.],\n",
       "       [0., 0., 0., 0., 0., 1., 0.],\n",
       "       [0., 0., 0., 0., 0., 1., 0.],\n",
       "       [0., 0., 0., 0., 0., 1., 0.]], dtype=float32)>"
      ]
     },
     "execution_count": 53,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 4.26 ms (started: 2021-08-18 02:50:10 +08:00)\n"
     ]
    }
   ],
   "source": [
    " cat_one_hot"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "published-teacher",
   "metadata": {},
   "source": [
    "根据经验，如果类别数低于 10，则通常采用 one-hot 编码（但您的里程可能会有所不同！）。 如果类别数大于 50（使用哈希桶时经常出现这种情况），则嵌入通常更可取。 在 10 到 50 个类别中，您可能希望对这两个选项进行试验，看看哪一个最适合您的用例。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "arctic-industry",
   "metadata": {},
   "source": [
    "### 使用嵌入编码分类特征"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "electric-supplement",
   "metadata": {},
   "source": [
    "嵌入是表示类别的可训练密集向量。默认情况下，嵌入是随机初始化的，因此例如“NEAR BAY”类别最初可以由一个随机向量表示，例如 [0.131, 0.890]，而“NEAR OCEAN”类别可能由另一个随机向量表示，例如 [ 0.631, 0.791]。在此示例中，我们使用 2D 嵌入，但维度数是您可以调整的超参数。由于这些嵌入是可训练的，它们会在训练过程中逐渐改进；并且由于它们代表非常相似的类别，因此梯度下降肯定会最终将它们推得更近，同时它会倾向于将它们从“内陆”类别的嵌入中移开（见图 13-4）。事实上，表示越好，神经网络就越容易做出准确的预测，因此训练倾向于使嵌入成为类别的有用表示。这称为表示学习（我们将在第 17 章中看到其他类型的表示学习）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "healthy-financing",
   "metadata": {},
   "source": [
    "![](https://tva1.sinaimg.cn/large/008i3skNgy1gtkd9l785nj618o0l476e02.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "female-lover",
   "metadata": {},
   "source": [
    "Keras 提供了一个 keras.layers.Embedding 层来处理嵌入矩阵（默认情况下可训练）； 当层被创建时，它随机初始化嵌入矩阵，然后当它被一些类别索引调用时，它返回嵌入矩阵中这些索引处的行："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "black-departure",
   "metadata": {},
   "outputs": [],
   "source": [
    "embedding = keras.layers.Embedding(input_dim=len(vocab) + num_oov_buckets,\n",
    "                                   output_dim=embedding_dim)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "suburban-tooth",
   "metadata": {},
   "outputs": [],
   "source": [
    "embedding(cat_indices)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "resident-journalist",
   "metadata": {},
   "source": [
    "将所有内容放在一起，我们现在可以创建一个 Keras 模型，该模型可以处理分类特征（以及常规数值特征）并学习每个类别（以及每个 oov 存储桶）的嵌入："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "russian-bubble",
   "metadata": {},
   "outputs": [],
   "source": [
    "regular_inputs = keras.layers.Input(shape=[8])\n",
    "categories = keras.layers.Input(shape=[], dtype=tf.string)\n",
    "cat_indices = keras.layers.Lambda(lambda cats: table.lookup(cats))(categories)\n",
    "cat_embed = keras.layers.Embedding(input_dim=6, output_dim=2)(cat_indices)\n",
    "encoded_inputs = keras.layers.concatenate([regular_inputs, cat_embed])\n",
    "outputs = keras.layers.Dense(1)(encoded_inputs)\n",
    "model = keras.models.Model(inputs=[regular_inputs, categories],\n",
    "outputs=[outputs])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "played-biography",
   "metadata": {},
   "source": [
    "该模型需要两个输入：一个包含每个实例八个数字特征的常规输入，以及一个分类输入（每个实例包含一个分类特征）。 它使用 Lambda 层查找每个类别的索引，然后查找这些索引的嵌入。 接下来，它将嵌入和常规输入连接起来，以提供编码输入，这些输入准备好馈送到神经网络。 此时我们可以添加任何类型的神经网络，但我们只需添加一个密集输出层，然后我们创建 Keras 模型。\n",
    "当 keras.layers.TextVectorization 层可用时，您可以调用它的 adapt() 方法使其从数据样本中提取词汇表（它将负责为您创建查找表）。 然后您可以将其添加到您的模型中，它将执行索引查找（替换前面代码示例中的 Lambda 层）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "separated-couple",
   "metadata": {},
   "source": [
    "> One-hot 编码后跟一个 Dense 层（没有激活函数和偏差）相当于一个嵌入层。 然而，嵌入层使用更少的计算（当嵌入矩阵的大小增加时，性能差异变得明显）。 Dense 层的权重矩阵起到了嵌入矩阵的作用。 例如，使用大小为 20 的 one-hot 向量和具有 10 个单位的 Dense 层等效于使用 input_dim=20 和 output_dim=10 的嵌入层。 因此，使用比 Embedding 层之后的层中的单元数更多的嵌入维度将是一种浪费。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "mexican-waterproof",
   "metadata": {},
   "source": [
    "现在让我们更仔细地看看 Keras 预处理层。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "technical-convert",
   "metadata": {},
   "source": [
    "### Keras 预处理层"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "detailed-grocery",
   "metadata": {},
   "source": [
    "TensorFlow 团队正在致力于提供一组标准的 Keras 预处理层。 当您阅读本文时，它们可能已经可用； 但是，届时 API 可能会略有变化，因此如果出现任何异常行为，请参阅本章的笔记本。 这个新的 API 可能会取代现有的 Feature Columns API，后者更难使用且不太直观（如果你想了解更多关于 Feature Columns API 的信息，请查看本章的笔记本）。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "configured-atmosphere",
   "metadata": {},
   "source": [
    "我们已经讨论了其中的两个层：将执行特征标准化的 keras.layers.Normalization 层（它相当于我们之前定义的 Standardization 层），以及能够将输入中的每个单词编码为 它在词汇表中的索引。 在这两种情况下，您都可以创建层，使用数据样本调用它的 adapt() 方法，然后在模型中正常使用该层。 其他预处理层将遵循相同的模式。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "passive-hypothesis",
   "metadata": {},
   "source": [
    "该 API 还将包括一个 keras.layers.Discretization 层，它将连续数据切割成不同的 bin 并将每个 bin 编码为一个单热向量。 例如，您可以使用它将价格离散为三类（低、中、高），它们将被编码为 [1, 0, 0]、[0, 1, 0] 和 [0, 0, 1] ]， 分别。 当然，这会丢失很多信息，但在某些情况下，它可以帮助模型检测在仅查看连续值时不明显的模式。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "binding-beginning",
   "metadata": {},
   "source": [
    "> Discretization 层是不可微的，它应该只在模型开始时使用。 实际上，模型的预处理层在训练过程中会被冻结，因此它们的参数不会受到梯度下降的影响，因此它们不需要可微。 这也意味着您不应该直接在自定义预处理层中使用 Embedding 层，如果您希望它可训练：相反，应将其单独添加到您的模型中，如前面的代码示例所示。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "recovered-myanmar",
   "metadata": {},
   "source": [
    "还可以使用 PreprocessingStage 类链接多个预处理层。 例如，以下代码将创建一个预处理管道，该管道将首先标准化输入，然后离散化它们（这可能会让您想起 Scikit-Learn 管道）。 将此管道调整为数据样本后，您可以像模型中的常规层一样使用它（但同样，仅在模型的开头，因为它包含不可微分的预处理层）："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "french-coral",
   "metadata": {},
   "outputs": [],
   "source": [
    "normalization = keras.layers.Normalization()\n",
    "discretization = keras.layers.Discretization([...])\n",
    "pipeline = keras.layers.PreprocessingStage([normalization, discretization])\n",
    "pipeline.adapt(data_sample)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eastern-guatemala",
   "metadata": {},
   "source": [
    "TextVectorization 层还可以选择输出字数向量而不是字索引。例如，如果词汇表包含三个单词，比如 [\"and\", \"basketball\", \"more\"]，那么文本 \"more and more\" 将映射到向量 [1, 0, 2]：单词 \" and”出现一次，“basketball”这个词根本没有出现，“more”这个词出现了两次。这种文本表示被称为词袋，因为它完全失去了词的顺序。像“and”这样的常用词在大多数文本中都有很大的价值，即使它们通常是最不有趣的（例如，在文本“越来越多的篮球”中，“篮球”这个词显然是最重要的，正是因为它是不是很常用的词）。因此，应以降低频繁词重要性的方式对字数进行归一化。一种常见的方法是将每个单词计数除以该单词出现的训练实例总数的对数。这种技术称为术语频率×逆文档频率（TF-IDF）。例如，假设单词“and”、“basketball”和“more”分别出现在训练集中的 200、10 和 100 个文本实例中：在这种情况下，最终向量将为 [1/log( 200), 0/log(10), 2/log(100)]，大约等于 [0.19, 0., 0.43]。 TextVectorization 层将（可能）有一个选项来执行 TF-IDF。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "instant-smooth",
   "metadata": {},
   "source": [
    "> 如果标准预处理层不足以完成您的任务，您仍然可以选择创建自己的自定义预处理层，就像我们之前对 Standardization 类所做的一样。 使用 adapt() 方法创建 keras.layers.PreprocessingLayer 类的子类，该方法应采用 data_sample 参数和可选的额外 reset_state 参数：如果为 True，则 adapt() 方法应在计算新状态之前重置任何现有状态 ; 如果为 False，它应该尝试更新现有状态。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "interstate-pitch",
   "metadata": {},
   "source": [
    "如您所见，这些 Keras 预处理层将使预处理变得更加容易！ 现在，无论您选择编写自己的预处理层还是使用 Keras（甚至使用 Feature Columns API），所有预处理都将即时完成。 然而，在训练期间，最好提前执行预处理。 让我们看看我们为什么要这样做以及我们将如何去做。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "id": "elegant-agenda",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['__builtins__',\n",
       " '__cached__',\n",
       " '__doc__',\n",
       " '__file__',\n",
       " '__loader__',\n",
       " '__name__',\n",
       " '__package__',\n",
       " '__path__',\n",
       " '__spec__',\n",
       " '_sys',\n",
       " 'image',\n",
       " 'image_dataset_from_directory',\n",
       " 'sequence',\n",
       " 'text',\n",
       " 'text_dataset_from_directory',\n",
       " 'timeseries_dataset_from_array']"
      ]
     },
     "execution_count": 56,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.39 ms (started: 2021-08-18 03:06:30 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dir(tf.keras.preprocessing)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "id": "appreciated-denial",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['DirectoryIterator',\n",
       " 'ImageDataGenerator',\n",
       " 'Iterator',\n",
       " 'NumpyArrayIterator',\n",
       " '__builtins__',\n",
       " '__cached__',\n",
       " '__doc__',\n",
       " '__file__',\n",
       " '__loader__',\n",
       " '__name__',\n",
       " '__package__',\n",
       " '__path__',\n",
       " '__spec__',\n",
       " '_sys',\n",
       " 'apply_affine_transform',\n",
       " 'apply_brightness_shift',\n",
       " 'apply_channel_shift',\n",
       " 'array_to_img',\n",
       " 'img_to_array',\n",
       " 'load_img',\n",
       " 'random_brightness',\n",
       " 'random_channel_shift',\n",
       " 'random_rotation',\n",
       " 'random_shear',\n",
       " 'random_shift',\n",
       " 'random_zoom',\n",
       " 'save_img',\n",
       " 'smart_resize']"
      ]
     },
     "execution_count": 57,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.64 ms (started: 2021-08-18 03:06:47 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dir(tf.keras.preprocessing.image)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "id": "lonely-carter",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['Tokenizer',\n",
       " '__builtins__',\n",
       " '__cached__',\n",
       " '__doc__',\n",
       " '__file__',\n",
       " '__loader__',\n",
       " '__name__',\n",
       " '__package__',\n",
       " '__path__',\n",
       " '__spec__',\n",
       " '_sys',\n",
       " 'hashing_trick',\n",
       " 'one_hot',\n",
       " 'text_to_word_sequence',\n",
       " 'tokenizer_from_json']"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.74 ms (started: 2021-08-18 03:07:01 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dir(tf.keras.preprocessing.text)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "id": "mighty-intensity",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['TimeseriesGenerator',\n",
       " '__builtins__',\n",
       " '__cached__',\n",
       " '__doc__',\n",
       " '__file__',\n",
       " '__loader__',\n",
       " '__name__',\n",
       " '__package__',\n",
       " '__path__',\n",
       " '__spec__',\n",
       " '_sys',\n",
       " 'make_sampling_table',\n",
       " 'pad_sequences',\n",
       " 'skipgrams']"
      ]
     },
     "execution_count": 59,
     "metadata": {},
     "output_type": "execute_result"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 3.18 ms (started: 2021-08-18 03:07:14 +08:00)\n"
     ]
    }
   ],
   "source": [
    "dir(tf.keras.preprocessing.sequence)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "infinite-graphic",
   "metadata": {},
   "source": [
    "## TF变换"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "essential-battery",
   "metadata": {},
   "source": [
    "如果预处理在计算上是昂贵的，那么在训练之前处理它而不是在运行中处理它可能会给你一个显着的加速：在训练前每个实例只对数据进行一次预处理，而不是在训练期间每个实例和每个时代一次。 如前所述，如果数据集小到可以放入 RAM，您可以使用它的 cache() 方法。 但如果它太大，那么像 Apache Beam 或 Spark 这样的工具会有所帮助。 它们让您可以在大量数据上运行高效的数据处理管道，甚至分布在多个服务器上，因此您可以在训练前使用它们对所有训练数据进行预处理。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "acceptable-clause",
   "metadata": {},
   "source": [
    "这很有效，确实可以加快训练速度，但有一个问题：一旦您的模型经过训练，假设您想将其部署到移动应用程序。 在这种情况下，您需要在您的应用程序中编写一些代码，以在将数据提供给模型之前对其进行预处理。 假设您还想将模型部署到 TensorFlow.js 以便它在 Web 浏览器中运行？ 同样，您将需要编写一些预处理代码。 这可能成为维护的噩梦：每当您想要更改预处理逻辑时，您都需要更新您的 Apache Beam 代码、您的移动应用程序代码和您的 JavaScript 代码。 这不仅耗时，而且容易出错：您最终可能会发现训练前执行的预处理操作与在您的应用程序或浏览器中执行的预处理操作之间存在细微差别。 这种训练/服务偏差会导致错误或性能下降。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "medieval-madrid",
   "metadata": {},
   "source": [
    "一种改进是采用经过训练的模型（根据由 Apache Beam 或 Spark 代码预处理的数据进行训练），并在将其部署到您的应用程序或浏览器之前，添加额外的预处理层以进行动态预处理。这肯定更好，因为现在你只有两个版本的预处理代码：Apache Beam 或 Spark 代码，以及预处理层的代码。\n",
    "\n",
    "但是，如果您可以只定义一次预处理操作呢？这就是 TF Transform 的设计目的。它是 TensorFlow Extended (TFX) 的一部分，这是一个用于生产 TensorFlow 模型的端到端平台。首先，要使用TF Transform等TFX组件，必须安装；它不与 TensorFlow 捆绑在一起。然后，您只需定义一次预处理函数（在 Python 中），使用 TF 变换函数进行缩放、分桶等。你也可以使用任何你需要的 TensorFlow 操作。如果我们只有两个特征，这个预处理函数可能看起来像这样："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "mobile-terminal",
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow_transform as tft\n",
    "\n",
    "def preprocess(inputs): # inputs = a batch of input features\n",
    "    median_age = inputs[\"housing_median_age\"]\n",
    "    ocean_proximity = inputs[\"ocean_proximity\"]\n",
    "    standardized_age = tft.scale_to_z_score(median_age)\n",
    "    ocean_proximity_id = tft.compute_and_apply_vocabulary(ocean_proximity)\n",
    "    return {\n",
    "    \"standardized_median_age\": standardized_age,\n",
    "    \"ocean_proximity_id\": ocean_proximity_id\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "practical-huntington",
   "metadata": {},
   "source": [
    "接下来，TF Transform 允许您使用 Apache Beam 将此 preprocess() 函数应用到整个训练集（它提供了一个 AnalyzeAndTransformDataset 类，您可以在 Apache Beam 管道中用于此目的）。 在此过程中，它还将计算整个训练集的所有必要统计信息：在本例中，是 Housing_median_age 特征的均值和标准差，以及 Ocean_proximity 特征的词汇表。 计算这些统计数据的组件称为分析器。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "alternative-condition",
   "metadata": {},
   "source": [
    "## TensorFlow 数据集 (TFDS) 项目"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "amateur-growth",
   "metadata": {},
   "source": [
    "TensorFlow Datasets 项目使下载常见数据集变得非常容易，从 MNIST 或 Fashion MNIST 等小型数据集到 ImageNet 等大型数据集（您将需要相当多的磁盘空间！）。 该列表包括图像数据集、文本数据集（包括翻译数据集）和音频和视频数据集。 您可以访问 https://homl.info/tfds 查看完整列表以及每个数据集的描述。\n",
    "\n",
    "TFDS 未与 TensorFlow 捆绑，因此您需要安装 tensorflow 数据集库（例如，使用 pip）。 然后调用 tfds.load() 函数，它会下载你想要的数据（除非它之前已经下载过）并将数据作为数据集字典返回（通常一个用于训练，一个用于测试，但这取决于 您选择的数据集）。 例如，让我们下载 MNIST："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "id": "broke-attribute",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[1mDownloading and preparing dataset Unknown size (download: Unknown size, generated: Unknown size, total: Unknown size) to /home/huangwei/tensorflow_datasets/mnist/3.0.1...\u001b[0m\n"
     ]
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "724817f1a7d34aed91d7eeacb103a3c6",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Dl Completed...: 0 url [00:00, ? url/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "1ec3f6199e384c59a0d095a321230e83",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Dl Size...: 0 MiB [00:00, ? MiB/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "9c6b81b68aab43c5a804a082dab58088",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Extraction completed...: 0 file [00:00, ? file/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "c83760649ffe47119ffaa4886bf401c5",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Generating splits...:   0%|          | 0/2 [00:00<?, ? splits/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "0d1b34ebb8ff497199b0d97d9c0c59c4",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Generating train examples...: 0 examples [00:00, ? examples/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "370d76a4c9fb4373843ad1b691fc5220",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Shuffling mnist-train.tfrecord...:   0%|          | 0/60000 [00:00<?, ? examples/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "ab7a8f07f28f4d5bbce2dd3011108e2a",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Generating test examples...: 0 examples [00:00, ? examples/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "3a1c2e74f7ca4b93bb0d9dd744c25111",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Shuffling mnist-test.tfrecord...:   0%|          | 0/10000 [00:00<?, ? examples/s]"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[1mDataset mnist downloaded and prepared to /home/huangwei/tensorflow_datasets/mnist/3.0.1. Subsequent calls will reuse this data.\u001b[0m\n",
      "time: 1min 46s (started: 2021-08-18 03:15:24 +08:00)\n"
     ]
    }
   ],
   "source": [
    "import tensorflow_datasets as tfds\n",
    "\n",
    "dataset = tfds.load(name=\"mnist\")\n",
    "mnist_train, mnist_test = dataset[\"train\"], dataset[\"test\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "satisfied-myrtle",
   "metadata": {},
   "source": [
    "然后，您可以应用您想要的任何转换（通常是混洗、批处理和预取），并准备好训练您的模型。 这是一个简单的例子："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "hairy-silly",
   "metadata": {},
   "outputs": [],
   "source": [
    "mnist_train = mnist_train.shuffle(10000).batch(32).prefetch(1)\n",
    "for item in mnist_train:\n",
    "images = item[\"image\"]\n",
    "labels = item[\"label\"] [...]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "tough-vitamin",
   "metadata": {},
   "source": [
    "load() 函数会对其下载的每个数据分片进行混洗（仅适用于训练集）。 这可能还不够，所以最好再打乱训练数据。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "loved-sunday",
   "metadata": {},
   "source": [
    "请注意，数据集中的每个项目都是一个包含特征和标签的字典。 但是 Keras 期望每个项目都是一个包含两个元素（同样是特征和标签）的元组。 您可以使用 map() 方法转换数据集，如下所示："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "id": "laden-trace",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "time: 37.2 ms (started: 2021-08-18 03:17:54 +08:00)\n"
     ]
    }
   ],
   "source": [
    "mnist_train = mnist_train.shuffle(10000).batch(32)\n",
    "mnist_train = mnist_train.map(lambda items: (items[\"image\"], items[\"label\"]))\n",
    "mnist_train = mnist_train.prefetch(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "specified-journey",
   "metadata": {},
   "source": [
    "但是通过设置 as_supervised=True 来让 load() 函数为你做这件事更简单（显然这仅适用于标记数据集）。 如果需要，您还可以指定批量大小。 然后您可以将数据集直接传递给您的 tf.keras 模型："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "agricultural-durham",
   "metadata": {},
   "outputs": [],
   "source": [
    "dataset = tfds.load(name=\"mnist\", batch_size=32, as_supervised=True)\n",
    "mnist_train = dataset[\"train\"].prefetch(1)\n",
    "model = keras.models.Sequential([...])\n",
    "model.compile(loss=\"sparse_categorical_crossentropy\", optimizer=\"sgd\")\n",
    "model.fit(mnist_train, epochs=5)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "foreign-preserve",
   "metadata": {},
   "source": [
    "这是一个相当技术性的章节，你可能会觉得它离神经网络的抽象美有点远，但事实是深度学习通常涉及大量数据，并且知道如何有效地加载、解析和预处理它 是一项至关重要的技能。 在下一章中，我们将研究卷积神经网络，它是用于图像处理和许多其他应用的最成功的神经网络架构之一。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "aquatic-cameroon",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python [conda env:tensorflow]",
   "language": "python",
   "name": "conda-env-tensorflow-py"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
